{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# MCP + LangGraph Hands-On Tutorial\n",
    "\n",
    "- Author: [Teddy Notes](https://youtube.com/c/teddynote)\n",
    "- Lecture: [Fastcampus RAG trick notes](https://fastcampus.co.kr/data_online_teddy)\n",
    "\n",
    "**References**\n",
    "- https://modelcontextprotocol.io/introduction\n",
    "- https://github.com/langchain-ai/langchain-mcp-adapters"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## configure\n",
    "\n",
    "Refer to the installation instructions below to install `uv`.\n",
    "\n",
    "**How to install `uv`**\n",
    "\n",
    "```bash\n",
    "# macOS/Linux\n",
    "curl -LsSf https://astral.sh/uv/install.sh | sh\n",
    "\n",
    "# Windows (PowerShell)\n",
    "irm https://astral.sh/uv/install.ps1 | iex\n",
    "```\n",
    "\n",
    "Install **dependencies**\n",
    "\n",
    "```bash\n",
    "uv pip install -r requirements.txt\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Gets the environment variables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## MultiServerMCPClient"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run `mcp_server_remote.py` in advance. Open a terminal with the virtual environment activated and run the server.\n",
    "\n",
    "> Command\n",
    "```bash\n",
    "source .venv/bin/activate\n",
    "python mcp_server_remote.py\n",
    "```\n",
    "\n",
    "Create and terminate a temporary Session connection using `async with`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from utils import ainvoke_graph, astream_graph\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "async with MultiServerMCPClient(\n",
    "    {\n",
    "        \"weather\": {\n",
    "            # Must match the server's port (port 8005)\n",
    "            \"url\": \"http://localhost:8005/sse\",\n",
    "            \"transport\": \"sse\",\n",
    "        }\n",
    "    }\n",
    ") as client:\n",
    "    print(client.get_tools())\n",
    "    agent = create_react_agent(model, client.get_tools())\n",
    "    answer = await astream_graph(\n",
    "        agent, {\"messages\": \"What's the weather like in Seoul?\"}\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You might notice that you can't access the tool because the session is closed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's change that to accessing the tool while maintaining an Async Session."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 1. Create client\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"weather\": {\n",
    "            \"url\": \"http://localhost:8005/sse\",\n",
    "            \"transport\": \"sse\",\n",
    "        }\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. Explicitly initialize connection (this part is necessary)\n",
    "# Initialize\n",
    "await client.__aenter__()\n",
    "\n",
    "# Now tools are loaded\n",
    "print(client.get_tools())  # Tools are displayed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create an agent with langgraph(`create_react_agent`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create agent\n",
    "agent = create_react_agent(model, client.get_tools())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run the graph to see the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stdio method\n",
    "\n",
    "The Stdio method is intended for use in a local environment.\n",
    "\n",
    "- Use standard input/output for communication"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_mcp_adapters.tools import load_mcp_tools\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# Initialize Anthropic's Claude model\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# Set up StdIO server parameters\n",
    "# - command: Path to Python interpreter\n",
    "# - args: MCP server script to execute\n",
    "server_params = StdioServerParameters(\n",
    "    command=\"./.venv/bin/python\",\n",
    "    args=[\"mcp_server_local.py\"],\n",
    ")\n",
    "\n",
    "# Use StdIO client to communicate with the server\n",
    "async with stdio_client(server_params) as (read, write):\n",
    "    # Create client session\n",
    "    async with ClientSession(read, write) as session:\n",
    "        # Initialize connection\n",
    "        await session.initialize()\n",
    "\n",
    "        # Load MCP tools\n",
    "        tools = await load_mcp_tools(session)\n",
    "        print(tools)\n",
    "\n",
    "        # Create agent\n",
    "        agent = create_react_agent(model, tools)\n",
    "\n",
    "        # Stream agent responses\n",
    "        await astream_graph(agent, {\"messages\": \"What's the weather like in Seoul?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Use MCP server with RAG deployed\n",
    "\n",
    "- File: `mcp_server_rag.py`\n",
    "\n",
    "Use the `mcp_server_rag.py` file that we built with langchain in advance.\n",
    "\n",
    "It uses stdio communication to get information about the tools, where it gets the `retriever` tool, which is the tool defined in `mcp_server_rag.py`. This file **doesn't** need to be running on the server beforehand."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "from langchain_mcp_adapters.tools import load_mcp_tools\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "from utils import astream_graph\n",
    "\n",
    "# Initialize Anthropic's Claude model\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# Set up StdIO server parameters for the RAG server\n",
    "server_params = StdioServerParameters(\n",
    "    command=\"./.venv/bin/python\",\n",
    "    args=[\"./mcp_server_rag.py\"],\n",
    ")\n",
    "\n",
    "# Use StdIO client to communicate with the RAG server\n",
    "async with stdio_client(server_params) as (read, write):\n",
    "    # Create client session\n",
    "    async with ClientSession(read, write) as session:\n",
    "        # Initialize connection\n",
    "        await session.initialize()\n",
    "\n",
    "        # Load MCP tools (in this case, the retriever tool)\n",
    "        tools = await load_mcp_tools(session)\n",
    "\n",
    "        # Create and run the agent\n",
    "        agent = create_react_agent(model, tools)\n",
    "\n",
    "        # Stream agent responses\n",
    "        await astream_graph(\n",
    "            agent,\n",
    "            {\n",
    "                \"messages\": \"Search for the name of the generative AI developed by Samsung Electronics\"\n",
    "            },\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Use a mix of SSE and Stdio methods\n",
    "\n",
    "- File: `mcp_server_rag.py` communicates over Stdio\n",
    "- `langchain-dev-docs` communicates via SSE\n",
    "\n",
    "Use a mix of SSE and Stdio methods."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# Initialize Anthropic's Claude model\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# 1. Create multi-server MCP client\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"document-retriever\": {\n",
    "            \"command\": \"./.venv/bin/python\",\n",
    "            # Update with the absolute path to mcp_server_rag.py file\n",
    "            \"args\": [\"./mcp_server_rag.py\"],\n",
    "            # Communicate via stdio (using standard input/output)\n",
    "            \"transport\": \"stdio\",\n",
    "        },\n",
    "        \"langchain-dev-docs\": {\n",
    "            # Make sure the SSE server is running\n",
    "            \"url\": \"https://teddynote.io/mcp/langchain/sse\",\n",
    "            # Communicate via SSE (Server-Sent Events)\n",
    "            \"transport\": \"sse\",\n",
    "        },\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. Initialize connection explicitly through async context manager\n",
    "await client.__aenter__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create an agent using `create_react_agent` in langgraph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "prompt = (\n",
    "    \"You are a smart agent. \"\n",
    "    \"Use `retriever` tool to search on AI related documents and answer questions.\"\n",
    "    \"Use `langchain-dev-docs` tool to search on langchain / langgraph related documents and answer questions.\"\n",
    "    \"Answer in English.\"\n",
    ")\n",
    "agent = create_react_agent(\n",
    "    model, client.get_tools(), prompt=prompt, checkpointer=MemorySaver()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use the `retriever` tool defined in `mcp_server_rag.py` that you built to perform the search."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"Use the `retriever` tool to search for the name of the generative AI developed by Samsung Electronics\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time, we'll use the `langchain-dev-docs` tool to perform the search."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"Please tell me about the definition of self-rag by referring to the langchain-dev-docs\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use `MemorySaver` to maintain short-term memory, so multi-turn conversations are possible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\"messages\": \"Summarize the previous content in bullet points\"},\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LangChain-integrated tools + MCP tools\n",
    "\n",
    "Here we confirm that tools integrated into LangChain can be used in conjunction with existing MCP-only tools."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "# Initialize the Tavily search tool (news type, news from the last 3 days)\n",
    "tavily = TavilySearchResults(max_results=3, topic=\"news\", days=3)\n",
    "\n",
    "# Use it together with existing MCP tools\n",
    "tools = client.get_tools() + [tavily]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create an agent using `create_react_agent` in langgraph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "prompt = \"You are a smart agent with various tools. Answer questions in English.\"\n",
    "agent = create_react_agent(model, tools, prompt=prompt, checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Perform a search using the newly added `tavily` tool."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent, {\"messages\": \"Tell me about today's news for me\"}, config=config\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that the `retriever` tool is working smoothly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"Use the `retriever` tool to search for the name of the generative AI developed by Samsung Electronics\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Smithery MCP Server\n",
    "\n",
    "- Link: https://smithery.ai/\n",
    "\n",
    "List of tools used:\n",
    "\n",
    "- Sequential Thinking: https://smithery.ai/server/@smithery-ai/server-sequential-thinking\n",
    "  - MCP server providing tools for dynamic and reflective problem-solving through structured thinking processes\n",
    "- Desktop Commander: https://smithery.ai/server/@wonderwhy-er/desktop-commander\n",
    "  - Run terminal commands and manage files with various editing capabilities. Coding, shell and terminal, task automation\n",
    "\n",
    "**Note**\n",
    "\n",
    "- When importing tools provided by smithery in JSON format, you must set `\"transport\": \"stdio\"` as shown in the example below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# Initialize LLM model\n",
    "model = ChatAnthropic(model=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000)\n",
    "\n",
    "# 1. Create client\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"server-sequential-thinking\": {\n",
    "            \"command\": \"npx\",\n",
    "            \"args\": [\n",
    "                \"-y\",\n",
    "                \"@smithery/cli@latest\",\n",
    "                \"run\",\n",
    "                \"@smithery-ai/server-sequential-thinking\",\n",
    "                \"--key\",\n",
    "                \"your_smithery_api_key\",\n",
    "            ],\n",
    "            \"transport\": \"stdio\",  # Add communication using stdio method\n",
    "        },\n",
    "        \"desktop-commander\": {\n",
    "            \"command\": \"npx\",\n",
    "            \"args\": [\n",
    "                \"-y\",\n",
    "                \"@smithery/cli@latest\",\n",
    "                \"run\",\n",
    "                \"@wonderwhy-er/desktop-commander\",\n",
    "                \"--key\",\n",
    "                \"your_smithery_api_key\",\n",
    "            ],\n",
    "            \"transport\": \"stdio\",  # Add communication using stdio method\n",
    "        },\n",
    "        \"document-retriever\": {\n",
    "            \"command\": \"./.venv/bin/python\",\n",
    "            # Update with the absolute path to the mcp_server_rag.py file\n",
    "            \"args\": [\"./mcp_server_rag.py\"],\n",
    "            # Communication using stdio (standard input/output)\n",
    "            \"transport\": \"stdio\",\n",
    "        },\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. Explicitly initialize connection\n",
    "await client.__aenter__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create an agent using `create_react_agent` in langgraph."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "# Set up configuration\n",
    "config = RunnableConfig(recursion_limit=30, thread_id=3)\n",
    "\n",
    "# Create agent\n",
    "agent = create_react_agent(model, client.get_tools(), checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`Desktop Commander` 도구를 사용하여 터미널 명령을 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"Draw the folder structure including the current path as a tree. However, exclude the .venv folder from the output.\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll use the `Sequential Thinking` tool to see if we can accomplish a relatively complex task."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": (\n",
    "            \"Use the `retriever` tool to search for information about generative AI developed by Samsung Electronics, \"\n",
    "            \"and then use the `Sequential Thinking` tool to write a report.\"\n",
    "        )\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
